#!/usr/bin/php -q
<?php
/* 
  phpd Copyright (C) 2007, 2008 Andrew Rose
  rose.andrew@gmail.com
  http://andrewrose.co.uk
  http://andrew-rose.blogspot.com

  This program is free software; you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation; either version 2 of the License, or
  (at your option) any later version.

  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with this program; if not, write to the Free Software
  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
*/

define('APLCROOT', '/usr/lib/aplc/');
define('INICONF', 'phpd.ini');


if(php_sapi_name() != 'cli')
{
	exit("phpd will only work (predictably) using the CLI SAPI\n");
}

set_time_limit(0);
declare(ticks = 1);

if(!file_exists(APLCROOT.'/Aplc.php'))
{
	exit("Unable to open APLC library\n");
}

require_once('/usr/lib/aplc/Aplc.php');
Aplc::setupAutoloader();

class Phpd_Autoloader implements Aplc_Autoloader
{
	public function register()
	{
		spl_autoload_register(array($this, 'loader'));
	}

	public function loader($className)
	{
		/* check module/ */
		$file = 'module/' . strtolower(str_replace('_', '', str_replace('Phpd_Module', '', $className))) . '.php';
		if (file_exists($file))
		{
			require_once($file);
			return;
		}

		/* check application/ */
		$file = 'application/' . strtolower(str_replace('Phpd_Application_', '', $className)) . '/entrypoint.php';
		if(file_exists($file))
		{
			require_once($file);
			return;
		}

		/* check transport/ */
		$file = 'transport/' . strtolower(str_replace('Phpd_Transport_', '', $className)) . '.php';
		if(file_exists($file))
		{
			require_once($file);
			return;
		}
	}
}

interface Phpd_Transport
{
        public function init(Aplc_Registry $reg);
        public function request();
        public function read(&$data);
        public function response(&$data);
        public function close();
        public function deinit();
}

interface Phpd_Module
{
	public function init();
	public function request();
	public function response();
	public function cleanup();
	public function deinit();
}

class Phpd_Child extends Aplc_Daemon_Child
{
	public $server = "phpd/1.0";
	public $protocol = "HTTP/1.1";
	public $transport;
	public $request;
	public $response;
	public $headers = array();
	public $modules = array();
	public $data = '';
	public $http;

	public $status = 200;
	public $statusCodes = array(
		100 => 'Continue',
		200 => 'OK',
		500 => 'Internal Server Error',
		501 => 'Not Implemented',
		505 => 'HTTP Version Not Supported'
	);

	public function start()
	{
		if($this->reg->exists('_phpd.moduleLocation,_phpd.moduleLoadOrder'))
		{
			$moduleSkip = explode(',',$this->reg->get('_phpd.moduleSkip'));
			$modules = explode(',',$this->reg->get('_phpd.moduleLoadOrder'));
			if(is_array($modules))
			{
				foreach($modules as $module)
				{
					if(!in_array($module, $moduleSkip))
					{
						$class = 'Phpd_Module_'.$module;

						if(isset($this->modules[$module]))
						{
							$this->modules[$module]->deinit();
							unset($this->modules[$module]);
						}

						$this->modules[$module] = new $class;
						$this->modules[$module]->phpd = $this;
						if(!$this->modules[$module]->init())
						{
							$this->log->write('Unable to init module: '.$module);
							$this->log->write('Halted.');
							while(1)
							{
								if($this->shutdown)
								{
									return FALSE;
								}
								sleep(1);
							}
						}
					}
					else
					{
						$this->log->write('Skipping module startup: '.$module);
					}
				}
			}
		}

		$this->log->write('Startup was a success! Awaiting connections.');

		while(1)
		{
			if($this->transport->request())
			{
				$this->transport->read($this->request);
echo $this->request;

				$this->http->init($this->reg);
				$this->http->dissolveRequest($this->request);

				/*if(isset($this->http->headers['Content-Length']) && is_numeric($this->http->headers['Content-Length']))
				{
					$response = $this->protocol." 100 Continue\r\n\r\n";
					$this->transport->response($response);
					$this->transport->read($data, $this->http->headers['Content-Length']);
echo $data;
				}*/

				if($this->pokeModules('request'))
				{
					if(!$this->pokeModules('response'))
					{
						if($this->status == 200)
						{
							$this->status = 500;
						}
					}
				}
				else
				{
					if($this->status == 200)
					{
						$this->status = 500;
					}
				}

				if($this->status != 200)
				{
					$this->data = $this->statusCodes[$this->status];
				}

				$headers = $this->headers($this->status, $this->statusCodes[$this->status], strlen($this->data));
				$this->response = (string)$headers.$this->data;

				$this->transport->response($this->response);

				$this->transport->close();

				$this->pokeModules('cleanup'); // module cleanup
				$this->cleanup(); // phpd cleanup

				if($this->shutdown)
				{
					return TRUE;
				}
			}

			if($this->shutdown)
			{
				return TRUE;
			}
		}
	}

	public function pokeModules($action)
	{
		try
		{
			foreach($this->modules as $name => $module)
			{
				/* I've left out any function exists checks as it is implied in the module
				interface.. plus it wastes cpu cycles. */
				if(!$module->$action() && !$this->shutdown)
				{
					$this->log->write('Failed to poke module: '.$name.', with action: '.$action);
					return FALSE;
				}
			}
		}
                catch(Aplc_Exception $e)
                {
                        echo $e->getMessage();
			return FALSE;
                }

		return TRUE;
	}

	public function cleanup()
	{
		$this->headers = array();
		$this->request = '';
		$this->response = '';
		$this->data = '';
		$this->status = 200;
	}

	public function header($header)
	{
		$this->headers[] = $header;
	}

	public function headers($status, $title, $length=0, $mime='text/html')
	{
		$headers = $this->protocol.' '.$status.' '.$title."\r\n";
		$headers .= 'Server: '.$this->server."\r\n";
		$headers .= 'Date: '.date('D, d M Y H:i:s e', time())."\r\n";
		foreach($this->headers as $header)
		{
			$headers .= $header."\r\n";
		}
		$headers .= 'Content-Type: '.$mime."\r\n";
		$headers .= 'Content-Length: '.$length."\r\n";
		$headers .= 'Connection: close'."\r\n";
		return $headers . "\r\n";
	}

	public function reload()
	{
		$this->shutdown = TRUE;
	}

	public function registryPoke()
	{
	}

	public function shutdown()
	{
		$this->transport->deinit();
		$this->modules = array_reverse($this->modules, TRUE);
		$this->pokeModules('deinit');
	}
}

class Phpd extends Aplc_Daemon
{
	public $transport;
	public $phpd;

	public function start()
	{
		$http = new Aplc_Http;

		if(!$this->reg->exists('_phpd.transport'))
		{
			exit("Unable to determine transport!\n");
		}
		else
		{
			$class = 'Phpd_Transport_'.$this->reg->get('_phpd.transport');
			$this->transport = new $class;
			$this->transport->log = $this->log;
		}

		$this->childRespawn = TRUE;

		if(!$this->transport->init($this->reg->getReference('_phpd')))
		{
			exit('Unable to start '.$this->reg->get('_phpd.transport')." transport\n");
		}

		$this->phpd = new Phpd_Child;
		$this->phpd->http = $http;
		/* we want to keep the parent transport intact as we will be changing stuff by reference laters we clone now. */
		$this->phpd->transport = clone $this->transport;
		$this->phpd->transport->phpd = $this->phpd;

		while(1)
		{
			if($this->shutdown)
			{
				return TRUE;
			}

			if(!$this->child($this->phpd))
			{
				$this->log->write('Failed to fork child process!');
				return FALSE;
			}
		}

/* todo: move module deinit() here */
		unset($phpd);
	}

	public function registryPoke()
	{
		/* override _aplc.Daemon.maxFork with our preFork setting */
		$this->reg->set('_aplc.Daemon.maxFork', $this->reg->getOrSet('_phpd.preFork', FALSE, 1));
	}

	public function reload()
	{
		// we ask the transport to deinit as a parent which will poke the forked processes.
		// we have to skip the parent socket shutdown.. needs fixing.. mayube another flag?
		$this->transport->deinit();
		$this->initRegistry();
		$this->registryPoke();
	}

	public function shutdown()
	{
		/* we ping the child processes to push them out of the blocking state they may be in */
		if($this->reg->true('_phpd.ssl.on'))
		{
			$port = $this->reg->get('_phpd.ssl.port');
		}
		else
		{
			$port = $this->reg->get('_phpd.port');
		}

		for($i = $this->reg->get('_phpd.preFork'); $i; $i--)
		{
			@fsockopen($this->reg->get('_phpd.address'), $port, $errno, $errstr, 2);
		}

		$this->transport->deinit();
		$this->waitChildren();
	}
}

$moduleLoader = new Phpd_Autoloader;
$moduleLoader->register();

$phpd = new phpd;
$phpd->main();
